package asteroids.model;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;
import java.util.List;
import java.util.Random;
import java.util.Set;

import org.antlr.v4.runtime.RecognitionException;

import java.io.BufferedReader;
import java.io.InputStreamReader;

import asteroids.CollisionListener;
import asteroids.IFacade;
import asteroids.ModelException;
import asteroids.Vector;
import asteroids.model.programs.Program;
import asteroids.model.programs.parsing.*;
import asteroids.model.programs.parsing.expressions.Expression;
import asteroids.model.programs.parsing.statements.*;
import asteroids.model.programs.parsing.types.*;
/**
 * 
 * @author Thijs Dieltjens - Wim Konings
 * |2BCwaElt
 * |https://asteroids-ogp.googlecode.com/svn/trunk/
 * @version 2.0
 */
public class Facade implements IFacade<World, Ship, Asteroid, Bullet, Program> {
	@Override
	public World createWorld(double width, double height)throws ModelException {
		try{
			World world = new World(width, height);
			return world;
		}
		catch(Exception e){
			throw new ModelException("Not a valid height or width.");
		}
	}

	@Override
	public double getWorldWidth(World world) throws ModelException{
		try{
			return world.getSize().getXCoordinate();
		}
		catch(Exception e){
			throw new ModelException("Could not retreive width.");
		}
	}

	@Override
	public double getWorldHeight(World world) throws ModelException{
		try{
			return world.getSize().getYCoordinate();
		}
		catch(Exception e){
			throw new ModelException("Could not retreive height.");
		}
	}

	@Override
	public Set<Ship> getShips(World world) throws ModelException{
		try{		
			return world.getSpaceObjectsOfType(Ship.class);

		}
		catch(Exception e){
			throw new ModelException("Could not retrieve ships.");
		}
	}

	@Override
	public Set<Asteroid> getAsteroids(World world) throws ModelException{
		try{		
			return world.getSpaceObjectsOfType(Asteroid.class);		
		}
		catch(Exception e){
			throw new ModelException("Could not retrieve asteroids.");
		}
	}

	@Override
	public Set<Bullet> getBullets(World world) throws ModelException{
		try{		
			return world.getSpaceObjectsOfType(Bullet.class);
		}
		catch(Exception e){
			throw new ModelException("Could not retrieve bullets.");
		}
	}

	@Override
	public void addShip(World world, Ship ship) throws ModelException{
		try{
			world.addSpaceObject(ship);
		}
		catch(Exception e){
			throw new ModelException("Could not add ship to the given world.");
		}

	}

	@Override
	public void addAsteroid(World world, Asteroid asteroid) throws ModelException{
		try{
			world.addSpaceObject(asteroid);
		}
		catch(Exception e){

			throw new ModelException("Invalid asteroid or world given.");

		}

	}

	@Override
	public void removeShip(World world, Ship ship)throws ModelException {
		try{
			world.removeSpaceObject(ship);
		}
		catch(Exception e){
			throw new ModelException("Invalid ship or world given.");
		}


	}

	@Override
	public void removeAsteroid(World world, Asteroid asteroid) throws ModelException{
		try{
			world.removeSpaceObject(asteroid);
		}
		catch(Exception e){
			throw new ModelException("Invalid asteroid or world given.");
		}

	}

	@Override
	public void evolve(World world, double dt,
			CollisionListener collisionListener)throws ModelException {
		try{
			world.evolve(dt, collisionListener);
		}
		catch(Exception e){
			throw new ModelException("Could not evolve the world.");

		}
	}

	@Override
	public Ship createShip(double x, double y, double xVelocity,
			double yVelocity, double radius, double direction, double mass)throws ModelException {

		Vector coordinates = new Vector(x,y);
		Vector velocities = new Vector(xVelocity,yVelocity);
		try{
			Ship ship = new Ship(coordinates,velocities, direction, mass, radius);
			return ship;
		}
		catch(Exception e){
			throw new ModelException("No valid arguments for this ship.");
		}
	}

	@Override
	public boolean isShip(Object o) {
		if(o instanceof Ship){
			return true;
		}
		return false;
	}

	@Override
	public double getShipX(Ship ship)throws ModelException {
		try{
			return ship.getCoordinates().getXCoordinate();
		}
		catch(Exception e){
			throw new ModelException("No valid ship given");
		}
	}

	@Override
	public double getShipY(Ship ship) throws ModelException{
		try{
			return ship.getCoordinates().getYCoordinate();
		}
		catch(Exception e){
			throw new ModelException("No valid ship given");
		}
	}

	@Override
	public double getShipXVelocity(Ship ship) throws ModelException{
		try{
			return ship.getVelocities().getXCoordinate();
		}
		catch(Exception e){
			throw new ModelException("No valid ship given");
		}
	}

	@Override
	public double getShipYVelocity(Ship ship) throws ModelException{
		try{
			return ship.getVelocities().getYCoordinate();
		}
		catch(Exception e){
			throw new ModelException("No valid ship given");
		}
	}

	@Override
	public double getShipRadius(Ship ship)throws ModelException {
		try{
			return ship.getRadius();
		}
		catch(Exception e){
			throw new ModelException("No valid ship given");
		}
	}

	@Override
	public double getShipDirection(Ship ship)throws ModelException {
		try{
			return ship.getDirection();
		}
		catch(Exception e){
			throw new ModelException("No valid ship given");
		}
	}

	@Override
	public double getShipMass(Ship ship) throws ModelException{
		try{
			return ship.getMass();
		}
		catch(Exception e){
			throw new ModelException("No valid ship given");
		}
	}

	@Override
	public World getShipWorld(Ship ship) throws ModelException{
		try{
			return ship.getWorld();
		}
		catch(Exception e){
			throw new ModelException("No valid ship given");
		}
	}

	@Override
	public boolean isShipThrusterActive(Ship ship)throws ModelException {
		try{
			return ship.isActiveThruster();
		}
		catch(Exception e){
			throw new ModelException("Something went wrong while accessing the ship's thruster.");
		}
	}

	@Override
	public void setThrusterActive(Ship ship, boolean active) throws ModelException{
		try{
			ship.setThruster(active);
		}
		catch( Exception e ){
			throw new ModelException("Something went wrong while setting the ShipThruster");
		}
	}

	@Override
	public void turn(Ship ship, double angle) {
		try{
			angle = Ship.correctAngle(angle);	
			ship.turn(angle);		
		}
		catch(Exception e ){
			throw new ModelException("Something went wrong wile turning the ship." + ship.toString());
		}
	}

	@Override
	public void fireBullet(Ship ship) throws ModelException{
		try{
			ship.fireBullet();
		}
		catch(Exception e){
			throw new ModelException("Something went wrong firing bullet");

		}

	}

	@Override
	public Asteroid createAsteroid(double x, double y, double xVelocity,
			double yVelocity, double radius) throws ModelException{
		try{
			Vector coordinates = new Vector(x,y);
			Vector velocities = new Vector(xVelocity,yVelocity);
			Asteroid asteroid = new Asteroid(coordinates, velocities, radius);
			return asteroid;
		}
		catch(Exception e ){

			throw new ModelException("Error creating asteroid");

		}
	}

	@Override
	public Asteroid createAsteroid(double x, double y, double xVelocity,
			double yVelocity, double radius, Random random) throws ModelException {
		try{
			Vector coordinates = new Vector(x,y);
			Vector velocities = new Vector(xVelocity,yVelocity);
			Asteroid asteroid = new Asteroid(coordinates, velocities, radius, random);
			return asteroid;
		}
		catch(Exception e){
			throw new ModelException("Error creating asteroid");
		}
	}

	@Override
	public boolean isAsteroid(Object o) {
		if(o.getClass() == Asteroid.class){
			return true;
		}

		return false;
	}

	@Override
	public double getAsteroidX(Asteroid asteroid) throws ModelException{
		try{
			return asteroid.getCoordinates().getXCoordinate();
		}
		catch(Exception e){
			throw new ModelException("Could not retrieve coordinate.");
		}
	}

	@Override
	public double getAsteroidY(Asteroid asteroid)throws ModelException {
		try{
			return asteroid.getCoordinates().getYCoordinate();
		}
		catch(Exception e){
			throw new ModelException("Could not retrieve coordinate");
		}
	}

	@Override
	public double getAsteroidXVelocity(Asteroid asteroid) throws ModelException{
		try{
			return asteroid.getVelocities().getXCoordinate();
		}
		catch(Exception e){
			throw new ModelException("Could not retrieve velocity.");
		}
	}

	@Override
	public double getAsteroidYVelocity(Asteroid asteroid) throws ModelException{
		try{
			return asteroid.getVelocities().getYCoordinate();
		}
		catch(Exception e){
			throw new ModelException("Could not retrieve velocity.");
		}
	}

	@Override
	public double getAsteroidRadius(Asteroid asteroid) throws ModelException{
		try{
			return asteroid.getRadius();
		}
		catch(Exception e){
			throw new ModelException("Could not get Radius.");
		}
	}

	@Override
	public double getAsteroidMass(Asteroid asteroid)throws ModelException {
		try{
			return asteroid.getMass();
		}
		catch(Exception e){
			throw new ModelException("Could not get Mass.");
		}
	}

	@Override
	public World getAsteroidWorld(Asteroid asteroid) throws ModelException{
		try{
			return asteroid.getWorld();
		}
		catch(Exception e){
			throw new ModelException("Could not get World.");
		}
	}

	@Override
	public boolean isBullets(Object o) {
		if(o.getClass() == Bullet.class){
			return true;
		}	
		return false;
	}

	@Override
	public double getBulletX(Bullet bullet) throws ModelException {
		try{
			return bullet.getCoordinates().getXCoordinate();
		}
		catch(Exception e){
			throw new ModelException("Could not get Xcoordinate");
		}	
	}

	@Override
	public double getBulletY(Bullet bullet) throws ModelException{
		try{
			return bullet.getCoordinates().getYCoordinate();
		}
		catch(Exception e){
			throw new ModelException("Could not get Ycoordinate");
		}	
	}

	@Override
	public double getBulletXVelocity(Bullet bullet) throws ModelException{
		try{
			return bullet.getVelocities().getXCoordinate();
		}
		catch(Exception e){
			throw new ModelException("Could not get xVelocity");
		}	
	}

	@Override
	public double getBulletYVelocity(Bullet bullet) throws ModelException{
		try{
			return bullet.getVelocities().getYCoordinate();
		}
		catch(Exception e){
			throw new ModelException("Could not get yVelocity");
		}	
	}

	@Override
	public double getBulletRadius(Bullet bullet) throws ModelException{
		try{
			return bullet.getRadius();
		}
		catch(Exception e){
			throw new ModelException("Invalid bullet given");
		}	
	}

	@Override
	public double getBulletMass(Bullet bullet)throws ModelException {
		try{
			return bullet.getMass();
		}
		catch(Exception e){
			throw new ModelException("Invalid bullet given");
		}	
	}

	@Override
	public World getBulletWorld(Bullet bullet) throws ModelException{
		try{
			return bullet.getWorld();
		}
		catch(Exception e){
			throw new ModelException("Invalid bullet given");
		}	
	}

	@Override
	public Ship getBulletSource(Bullet bullet) throws ModelException {
		try{
			return bullet.getSource();
		}
		catch(Exception e){
			throw new ModelException("Something went wrong retrieving the source of the bullet");
		}
	}

	@Override
	public asteroids.IFacade.ParseOutcome<Program> parseProgram(String text) {
		Factory<Expression, Statement, Type> factory = new Factory<Expression, Statement, Type>();
		ProgramParser<Expression, Statement, Type> parser = new ProgramParser<>(factory);
		try {
			parser.parse(text);
			List<String> errors = parser.getErrors();
			if(! errors.isEmpty()) {
				return ParseOutcome.failure(errors.get(0));
			} 
			else {
				return ParseOutcome.success(new Program(parser.getGlobals(), parser.getStatement())); 
			}
		} 
		catch(RecognitionException e) {
			return ParseOutcome.failure(e.getMessage());
		}
	}

	@Override
	public asteroids.IFacade.ParseOutcome<Program> loadProgramFromStream(
			InputStream is) throws IOException {
		//
		// To convert the InputStream to String we use the
		// Reader.read(char[] buffer) method. We iterate until the
		// Reader return -1 which means there's no more data to
		// read. We use the StringWriter class to produce the string.
		//

		Writer writer = new StringWriter();
		if(is!=null){
			char[] buffer = new char[1024];
			try {
				Reader reader = new BufferedReader(
						new InputStreamReader(is, "UTF-8"));
				int n;
				while ((n = reader.read(buffer)) != -1) {
					writer.write(buffer, 0, n);
				}
			} 
			finally {
				is.close();
			}
			return parseProgram(writer.toString());
		}
		else{
			throw new ModelException("Something went wrong with stream.");
		}
	}


	@Override
	public asteroids.IFacade.ParseOutcome<Program> loadProgramFromUrl(URL url)
			throws IOException {
		return loadProgramFromStream(url.openStream());

	}

	@Override
	public boolean isTypeCheckingSupported() {
		return true;
	}

	@Override
	public asteroids.IFacade.TypeCheckOutcome typeCheckProgram(Program program) {
		List<String> errors = program.getTypeCheckErrors();
		if(! errors.isEmpty()) {
			return TypeCheckOutcome.failure(errors.get(0));
		} 
		else {
			return TypeCheckOutcome.success(); 
		}

	}

	@Override
	public void setShipProgram(Ship ship, Program program) {
		ship.setProgram((Program) program.clone());
	}

}
